﻿////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// <copyright>Copyright 2008-2011 Andreas Huber Doenni</copyright>
// Distributed under the GNU General Public License version 2 (GPLv2).
// See accompanying file License.txt or copy at http://phuse.codeplex.com/license.
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

namespace Phuse.Mime
{
    using System;
    using System.Collections.Generic;
    using System.Globalization;

    /// <summary>Processes header fields containing a date, e.g. Date.</summary>
    internal sealed class DateHeaderFieldBodyProcessor : HeaderFieldBodyProcessor
    {
        private const string DefaultZone = "-0000";
        private const DateTimeStyles ParseMode =
            DateTimeStyles.AllowWhiteSpaces | DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal;

        private readonly List<char> rawDateTime = new List<char>(32);
        private int lastNonWhitespaceIndex = int.MinValue;
        private int lastWordStartIndex = int.MinValue;
        private bool inDateTime;
        private bool lastWasWhitespace;
        private readonly DateHeaderField result;

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        internal DateHeaderFieldBodyProcessor(string fieldName)
        {
            this.result = new DateHeaderField(fieldName);
        }

        internal sealed override void ProcessNormalBodyEvent(byte ev)
        {
            this.ProcessEvent(ev);
        }

        internal sealed override void ProcessQuotedBodyEvent(byte ev, bool isQuoteChar)
        {
            if (!isQuoteChar)
            {
                this.ProcessEvent(ev);
            }
        }

        internal sealed override void ProcessCommentBodyEvent(byte ev)
        {
            // Comments are deliberately ignored
        }

        internal sealed override HeaderField GetHeaderField()
        {
            // See http://www.ietf.org/rfc/rfc5322.txt, 4.3.
            if (this.lastWordStartIndex < 0)
            {
                return this.result;
            }

            var rawDateTimeArray = this.rawDateTime.ToArray();
            var date = new string(rawDateTimeArray, 0, this.lastWordStartIndex);
            var zone = new string(
                rawDateTimeArray, this.lastWordStartIndex, this.lastNonWhitespaceIndex - this.lastWordStartIndex + 1);

            try
            {
                this.result.DateUtc = DateTime.Parse(ValidateZone(date, zone), CultureInfo.InvariantCulture, ParseMode);
            }
            catch (FormatException)
            {
                // Deliberately ignored
            }

            return this.result;
        }

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        private void ProcessEvent(byte ev)
        {
            if (this.inDateTime)
            {
                switch (ev)
                {
                    case (byte)' ':
                    case (byte)'\t':
                        this.lastWasWhitespace = true;
                        break;
                    default:
                        if (this.lastWasWhitespace)
                        {
                            this.lastWordStartIndex = this.rawDateTime.Count;
                            this.lastWasWhitespace = false;
                        }

                        this.lastNonWhitespaceIndex = this.rawDateTime.Count;
                        break;
                }

                this.rawDateTime.Add((char)ev);
            }
            else
            {
                switch (ev)
                {
                    case (byte)'0':
                    case (byte)'1':
                    case (byte)'2':
                    case (byte)'3':
                    case (byte)'4':
                    case (byte)'5':
                    case (byte)'6':
                    case (byte)'7':
                    case (byte)'8':
                    case (byte)'9':
                        this.inDateTime = true;
                        this.rawDateTime.Add((char)ev);
                        break;
                    default:
                        // Ignore day of week
                        break;
                }
            }
        }

        private static string ValidateZone(string dateTime, string zone)
        {
            try
            {
                short number = short.Parse(zone, CultureInfo.InvariantCulture);

                if (zone.Length < 4)
                {
                    // The zone is probably missing
                    return dateTime + zone;
                }

                // Apparently Silverlight limits the supported timezone offset for parsing to +- 14 hours, which makes
                // sense but is not allowed if one follows RFC5322 to the letter. Offsets outside of +-14 hours are
                // extremely rare. When processed by this implementation, such offsets lead to a date that is off by a
                // couple of days. So it doesn't seem to be worth the additional effort to follow the RFC5322
                // interpretation.
                if ((number < -1400) || (number > 1400))
                {
                    zone = DefaultZone;
                }
            }
            catch (OverflowException)
            {
                zone = DefaultZone;
            }
            catch (FormatException)
            {
                switch (zone.ToUpperInvariant())
                {
                    case "UT":
                    case "GMT":
                        zone = "+0000";
                        break;
                    case "EDT":
                        zone = "-0400";
                        break;
                    case "EST":
                    case "CDT":
                        zone = "-0500";
                        break;
                    case "CST":
                    case "MDT":
                        zone = "-0600";
                        break;
                    case "MST":
                    case "PDT":
                        zone = "-0700";
                        break;
                    case "PST":
                        zone = "-0800";
                        break;
                    default:
                        if (zone.Contains(":"))
                        {
                            // The zone is probably missing
                            return dateTime + zone;
                        }
                        else
                        {
                            zone = "-0000";
                        }

                        break;
                }
            }

            if ((zone[0] != '+') && (zone[0] != '-'))
            {
                zone = '+' + zone;
            }

            var hours = zone.Substring(0, zone.Length - 2);
            var minutes = zone.Substring(zone.Length - 2);

            if (int.Parse(minutes, CultureInfo.InvariantCulture) > 59)
            {
                minutes = "59";
            }

            return dateTime + hours + ':' + minutes;
        }
    }
}
